package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {


    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;


    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /*
    这里的优惠券是一个表
    然后秒杀优惠券又是一个表，秒杀优惠券是在优惠券的基础上添加了时间限制和库存限制
    然后在数据库实现的时候，是一个一对一的关系，然后秒杀优惠券的vouchor_id 和优惠券表的id是一个一对一的关系。



    锁的实现，在秒杀的时候存在超卖问题，太多线程并发导致超卖，使用乐观锁来控制超卖问题，乐观锁适合处理更新问题
    在解决一人一单的时候，通过查询订单表里面的id和优惠券id来判断该用户是否已经购买过优惠券，这时候判断也会出现线程安全问题（多个线程同时进入判断），可能造成一个用户又购买多单
    而这里是插入问题（插入订单），所以选择用悲观锁synchronized，这里加锁比较复杂，一开始是对方法加锁，后来优化为对userid加锁，后来又判断了事务和锁的先后顺序等等
    所以逻辑就是先给用户id加锁，让用户不能买多个单，然后在出售的时候使用乐观锁，避免超卖


    以上是单体系统

    下面是分布式集群
    解决分布式tomcat集群下，多个jvm的情况下锁的情况，这时候nginx做负载均衡，需要分布式锁，不然还是会出现并发问题
    比如说两个请求，第一个给了A，第二个给了B，在一人一单的情况下，A给userid加了锁，并不影响B，所以AB都可以添加一次订单。



    redis锁的实现，因为多个tomcat公用一个redis，所以可以用redis来实现锁，用redis里面的setex机制

    这里有一个锁误删的机制，可以用删锁时候判断来避免



     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券，这边通过mybatis-plus的方法实现对秒杀优惠券的查询
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始！");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束！");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足！");
        }
        //这边有一个问题就是spring在释放锁和提交事务之间的问题
        Long userId = UserHolder.getUser().getId();
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
        //加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {

        Long userId = UserHolder.getUser().getId();


        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次！");
        }
        //5，扣减库存 这里是一个mybatis-plus的一种写法，设置库存-1然后匹配voucher_id等于voucherId
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).gt("stock", 0).update(); //where id = ? and stock > 0
        //这边明显会有并发问题，因为库存是一个多线程的共享资源，这边没有上锁肯定会有超减问题。
        //比如说此时库存还有2，然后这时候一共有10个线程同时判断库存>1然后都创建了订单，就超卖了
        if (!success) {
            //扣减库存失败
            return Result.fail("库存不足！");
        }

        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id

        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        return Result.ok(orderId);
    }
}
